<?php
/**
 * @package Waterwheel
 */
/* 
 *  Copyright 2010 Andrew Rickmann
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *  http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
*/


/**
 * The request class parses the URL to obtain the parameters of the request,
 * and determines how that request should be handled.
 *
 * @author Andrew Rickmann
 */
class request {

    /**
     * The URL requested, minus the domain details, without the trailing slash
     * This will be compaired to the regex provided in the request rules
     * when deciding which request handler to use
     *
     * @var String holds the unsplit request string
     */
    public $request_string = '';


    /**
     *
     * An associative array containing the constituent parts of the request, after they have been worked out from URL
     * They can also be passed directly to the constructor if a new request is needed
     *
     * @var Array
     */
    public $request_params = array();


    /**
     *
     * @var Array An array of the variables provided in the request
     */
    public $request_vars = null;

    /**
     *
     * @var Array Holds data generated by the request handler, e.g. an items object.
     */
    private $data = array();


    /**
     * This holds the details of the rules that will be used to decide which request handler should be used
     * each of the request rules must be an associative array with the following:
     * ['handler'] = callback (a string or an array that can be called using call_user_func
     * ['min_params'] = Integer - The minimum number of params to make the rule applicable, if excluded will default to 1
     * ['max_params'] = Integer - The maximum number of params to make the rule applicable, if excluded there will be no max
     * ['rules'] = Array - Regular expressions (without the surrounding /) applied to each param in turn, i.e. rules[0] is applied to params[0] missing rules / params are ignored.
     *
     * Use add_request_rule to insert into this array.
     *
     * @var Array Holds details of the rules for assigning each request handler
     */
    private $request_rules = array();



    /**
     *  You can pass the constructor two types of array
     *  1. An array of request vars, this is a non-associative array containing the parts of the URL you want to request to process
     *  For example:
     *  array('page','about') will return the same page / data as navigating to http://yoursite/page/about/
     *
     *  2. An array of request params, this is an associative array containing the processed results of the request
     *  @todo complete the example after the reshuffle.
     *
     *
     * @param Array $vars
     */
    function __construct( $vars = null ) {


	//Add default request rules
	$this->add_request_rule('test', 'test', '{type}/{data:balls}');

	//send request rules to plugins
	plugins::action('request_rules', $this);

	//if not overidden with specific settings, run the URL as normal
	if ( is_null($vars) ) {
	    //set the request vars up from the URL
	    $this->request_vars = $this->get_request_vars();
	    //process request vars
	    $this->process_request_vars( $this->request_vars );

	} elseif ( is_array( $vars ) && array_keys( $vars ) === range( 0 , count( $vars )-1 ) ) {

	    //if there are no non numeric and non-sequential keys in the array, assume it is non-associative
	    //in that case it cannot be the request params and must instead be request vars we are being passed
	    //this is used in the index.php file to load the request /error/ without using a redirect.
	    $this->request_vars = $vars;
	    //process request vars
	    $this->process_request_vars( $this->request_vars );

	} else {

	    $this->request_params = $vars;
	    $this->process_request_vars( array() );

	}


    }
    

    
    /**
     * Splits the URI into the variables provided.
     *
     * @return Array An array containing all the request variables
     */
    function get_request_vars() {

	$indexpath = pathinfo($_SERVER['PHP_SELF']);
	$index_dir = $indexpath['dirname'];
	$requestpath = ( $index_dir != '/') ? str_replace( $index_dir , '' , $_SERVER['REQUEST_URI'] ) : $_SERVER['REQUEST_URI'];

	//remove bookend slashes
	$requestpath = preg_replace('/^\/(.*)\/?$/U' , "$1" , $requestpath);

	//save the request string
	$this->request_string = $requestpath;

	//split the request up into vars
	$request_vars = explode( '/' , $requestpath);

	//make sure that all vars are urldecoded
	foreach( $request_vars as $i => $var ) {
	    $request_vars[$i] = urldecode($var);
	}

	return $request_vars;
    }


    /**
     *
     * Assesses the request vars to determine which request handler to use.
     * @todo complete the taxonomy section
     * 
     * @param Array $request_vars An array containing the variables used in the request
     */
    function process_request_vars( $request_vars ) {

	//if the processing has already been done, and the info just passed in
	//then the request type param should refer to the handler we need to pass
	//the data to. Pass it and let the handler resolve what to do with it
	if ( isset( $this->request_params ) && isset( $this->request_params['request_type'] ) ) {
	    if( isset( $this->request_rules[$this->request_params['request_type']] ) ) {
		call_user_func( $this->request_rules[$this->request_params['request_type']]['handler'] , $this );
		return;
	    } else {
		call_user_func( $this->request_rules['default']['handler'] , $this );
		return;
	    }
	}

	//create the array of request rule regex replacements
	//these will be used to convert the rule in to a regular expression
	$request_rules_regex = array();

	//add the rules that do not require a database connection to be created
	$request_rules_regex['slug'] = '[a-z]{1,1}[a-z0-9\-]*';
	$request_rules_regex['date'] = '(?P<year>(?:19|20)?\d\d)(?:\/(?P<month>(?:0[1-9]|1[012]))(?:\/(?P<day>(?:0[1-9]|[12][0-9]|3[01])))?)?';
	$request_rules_regex['year'] = '(?:19|20)?\d\d';
	$request_rules_regex['month'] = '(?:0[1-9]|1[012])';
	$request_rules_regex['day'] = '(?:0[1-9]|[12][0-9]|3[01])';

	//wrap the remainder in a try-catch
	//these just won't get loaded if there is no database available
	//this includes new regex rules added by plugins
	//as we could bundle plugins that require database access and these
	//would prevent the installer from working
	try{

	    //create the content type string
	    $types = new content_types();
	    $types->get();

	    $type_string = '';
	    foreach( $types as $type ){
		$type_string .= ( empty( $type_string ) ) ? $type->content_type_slug : '|' . $type->content_type_slug;
	    }

	    $request_rules_regex['content-type'] = $type_string;

	    //todo 	make this line work once the taxonomy classes have been added
	    //look into using shared memory functions to cache the regex
	    $request_rules_regex['taxonomy'] = 'NOTYETIMPLEMENTED';

	    //let plugins add additional regular expression replacements
	    plugins::filter('request_rules_regex', $request_rules_regex);

	} catch( waterwheelexception $e){

	}

	//now wrap all the rules in a named group
	foreach( $request_rules_regex as $request_rules_regex_key => $request_rules_regex_value ){
	    $request_rules_regex[$request_rules_regex_key] = '(?P<' . $request_rules_regex_key . '>' . $request_rules_regex_value . ')';
	}

	//loop through the request rules and use the first one that matches
	foreach( $this->request_rules as $rule ) {

	    //remove bookend slashes
	    $rule['rule'] = preg_replace('/^\/(.*)\/?$/U' , "$1" , $rule['rule']);
	    
	    //using the current rule, find all data instances and add them to the
	    //request_rules_regex
	    preg_match_all('/{data:([a-zA-Z0-9\-]*)}/' , $rule['rule'] , $data_matches);
	    preg_match_all('/{content-type:([a-zA-Z0-9\-]*)}/' , $rule['rule'] , $type_matches);

	    //create new rules for all the data instances in this particular request rule
	    foreach( $data_matches[1] as $data_item ){
		$request_rules_regex['data:' . $data_item] = '(?P<' . $data_item . '>[^\/]*)';
	    }

	    //create new rules for all the type:slug instances in this particular request rule
	    foreach( $type_matches[1] as $type_item ){
		$request_rules_regex['content-type:' . $type_item] = '(?P<content-type>' . $type_item . ')';
	    }


	    //setup the request rule so that it constitutes a regex
	    //replace the slashes with escaped versions
	    $rule_regex = str_replace('/','\/',$rule['rule']);
	    //loop through each regex and replace the key in the rule with the regex.
	    foreach( $request_rules_regex as $request_rules_regex_key => $request_rules_regex_replacement ){
		$rule_regex = str_replace('{' . $request_rules_regex_key . '}', $request_rules_regex_replacement, $rule_regex);
	    }

	    //finally - if there are any unreplaced placeholders,
	    //this rule is never going to match
	    //so we can just skip it
	    if ( preg_match( '/\/\{[a-zA-Z0-9\-]*\}\/?/' , $rule_regex ) ){
		continue;
	    }

	    //check the regexes
	    //if the regex doesn't match, then go no further
	    if ( !preg_match( '/^'. $rule_regex .'$/i' , $this->request_string , $param_matches ) ) {
		continue;
	    }

	    //if we are still here then the URL matches the regex
	    //first assign the matched variables to the params
	     foreach( $request_rules_regex as $request_rules_regex_key => $request_rules_regex_value ){
		$data_item = str_replace('data:','',$request_rules_regex_key);
		if ( isset( $param_matches[$data_item] ) ){
		    $this->request_params[$data_item ] = $param_matches[$data_item];
		}
	    }

	    call_user_func( $rule['handler'] , $this );
	    return;
	    
	}
    }

    /**
     *
     * Inserts a new request rule into the list of request rules
     *
     * @param String $name The name of the rule
     * @param String | Array $handler A callback to the handler function
     * @param String $rule A URL using either plain text or paramaters
     * @param String $description Describe the rule
     *
     * URL paramaters are a word wrapped in curly braces. For example: {slug}
     * These parameters will be replaced by regular expressions automatically
     * provided they are supported.
     *
     * You can also include generic, named, parameters of your own
     * these should be in the format {data:name} where 'name' is the name of the data
     * This name will be used to populate the request paramaters so that your request
     * handler can make use of it.
     *
     * So, if your rule is
     * /blog/{data:author}/
     *
     * You will be able to access the author attribute from $request->request_params['author']
     *
     * A URL paramater can only be used once per URL.
     *
     * Supported URL parameters are:
     * {date} - This will match /year/ /year/month/ or /year/month/day/
     * {slug} - This will match any text with letters, numbers, or a hyphen
     * {type} - This will match any content-type
     *
     */
    function add_request_rule( $name , $handler , $rule , $description = '' ) {

	//create a new blank array to hold all the rules
	$rules_array = array();

	//create the new rule array
	$rule_array = array();
	$rule_array['name'] = $name;
	$rule_array['handler'] = $handler;
	$rule_array['rule'] = $rule;
	$rule_array['description'] = $description;
	
	//populate the new rules array
	$rules_array[$name] = $rule_array;

	//merge the new rules array with the old
	//I'm doing it this way so that the new rule is
	//earlier in the array than the old
	//thereby potentially overriding any rules that already exist

	$this->request_rules = array_merge($rules_array, $this->request_rules);

    }

    /**
     *
     * Remove a request rule
     *
     * @param String $name The name of the rule to remove
     */
    function remove_request_rule( $name ){
	unset( $this->request_rules[$name] );
    }

    /**
     *
     * The default request handler. This will load the index file from the template
     * once I am in a position to do it.
     * @todo make this load the index file from the template
     *
     * @param request $request
     */
    function default_request_handler( request $request ){
	echo 'This should be loading the index file but isn\'t finished yet.';
    }


    /**
     * Magic method, used to get items from the data array e.g. $request->items returns $request->data['items'] if it exists
     *
     * @param String $var The name of the variable to get
     * @return Mixed the value of that variable or false if not found.
     */
    function __get( $var ) {
	if ( isset( $this->$var ) ) {
	    return $this->$var;
	} else if ( isset( $this->data[$var] ) ) {
	    return $this->data[$var];
	} else {
	    return false;
	}
    }


    /**
     * @todo figure out how to create permalinks from request strings
     */
    function permalink(){}




}
?>